﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Dynamic;
using System.Reflection;
using System.Collections;
using System.IO;
using System.Diagnostics.Contracts;

using King.Reflection;
using King.Extensions;
using King.Declarative;
using King.Traverse;
using System.Text.RegularExpressions;
using System.Text;

namespace King.Text {

    public sealed class TextEncoder : Stream {

        private static int Copy(
            byte[] source, ref int sourceOffset, int sourceCount,
            byte[] target, ref int targetOffset, int targetCount) {

            int sourceCapacity = sourceCount - sourceOffset;
            int targetCapacity = targetCount - targetOffset;
            int count = new[] { sourceCapacity, targetCapacity }.Min();

            Array.Copy(source, sourceOffset, target, targetOffset, count);

            sourceOffset += count;
            targetOffset += count;

            return count;
        }

        private int m_position;
        private TextReader m_textReader;
        private Encoder m_encoder;
        private byte[] m_byteBuffer;
        private int m_byteBufferOffset;
        private int m_byteBufferCount;
        private char[] m_characterBuffer;

        public TextEncoder(TextReader textReader, Encoding encoding, int charBufferSize = 1024 * 4) {
            m_textReader = textReader;
            m_position = 0;
            m_encoder = encoding.GetEncoder();

            m_characterBuffer = new char[charBufferSize];
            m_byteBuffer = new byte[encoding.GetMaxByteCount(charBufferSize)];
        }

        public override bool CanRead {
            get { return true; }
        }
        public override bool CanSeek {
            get { return true; }
        }
        public override bool CanWrite {
            get { return false; }
        }
        public override void Flush() {
            throw new InvalidOperationException();
        }
        public override long Length {
            get { throw new InvalidOperationException(); }
        }
        public override long Position {
            get { return m_position; }
            set { throw new InvalidOperationException(); }
        }
        public override long Seek(long offset, SeekOrigin origin) {
            throw new InvalidOperationException();
        }
        public override void SetLength(long value) {
            throw new InvalidOperationException();
        }
        public override void Write(byte[] buffer, int offset, int count) {
            throw new InvalidOperationException();
        }

        public override int Read(byte[] buffer, int offset, int count) {

            int copied = 0;

            while (true) {
                
                // copy encoded characters
                copied += Copy(
                    m_byteBuffer, ref m_byteBufferOffset, m_byteBufferCount,
                    buffer, ref offset, count
                );

                // bail if target buffer is full
                if (count - offset == 0)
                    return copied;

                // read a chunk of characters
                var read = m_textReader.Read(m_characterBuffer, 0, m_characterBuffer.Length);

                // there is nothing left to encode
                if (read == 0)
                    return copied;

                // encode a chunk of characters
                m_byteBufferCount =                             // the number of bytes encoded
                    m_encoder.GetBytes(
                        chars: m_characterBuffer,               // the characters to encode
                        charIndex: 0,                           // start encoding from the begining
                        charCount: read,                        // encode all the characters read, byte buffer is big enough
                        bytes: m_byteBuffer,                    // buffer to hold the encoding
                        byteIndex: 0,                           // overwrite previous encoding
                        flush: false                            // don't flush and lose partial encodings
                    );
                m_byteBufferOffset = 0;                         // start copying from the begining of the byte buffer
            }
        }
    }
    public sealed class TextGenerator : TextReader {

        private IEnumerator<char?> m_generator;
        private int? m_current;

        internal TextGenerator(IEnumerable<char?> generator)
            : this(generator.GetEnumerator()) {
        }
        internal TextGenerator(IEnumerator<char?> generator) {
            m_generator = generator;
        }

        private int? GenerateNextCharacter() {
            return m_generator.MoveNext() ? (int?)m_generator.Current : -1;
        }

        public bool NullCharacter {
            get { return m_current == null; }
        }
        public override int Peek() {
            if (m_current != null)
                return (int)m_current;

            while (m_current == null)
                m_current = GenerateNextCharacter();

            return (int)m_current;
        }
        public override int Read(char[] buffer, int index, int count) {
            Contract.Requires(buffer != null);
            Contract.Requires(index >= 0);
            Contract.Requires(count >= 0);
            Contract.Requires(buffer.Length - index >= count);
            Contract.Ensures(Contract.Result<int>() >= 0);
            Contract.Ensures(Contract.Result<int>() <= Contract.OldValue(count));
            Contract.EndContractBlock();

            // initialize number of characters returned
            int result = 0;

            // initialize enumerator and consume null characters
            Peek();

            while (true) {

                // break if no more characters
                if (m_current == -1)
                    break;

                // break if null is encountered (flush)
                if (m_current == null)
                    break;

                // consume character
                var currentChar = (char)m_current;
                buffer[index + result++] = currentChar;

                // break if a new line is read
                if (currentChar == '\r' || currentChar == '\n')
                    break;

                // break if the buffer is full
                if (result == count)
                    break;

                // generate next character
                m_current = GenerateNextCharacter();
            }

            m_current = null;
            return result;
        }
        public override int Read() {
            var result = Peek();
            if (result != -1)
                m_current = null;
            return result;
        }
    }

    [Declaration(
        SubstituteCharacters = DeclarationAttribute.AllCharacters,
        SubstituteTypes = DeclarationAttribute.AllAdaptedLeafTypes,
        UseDedicatedStack = true
    )]
    public class TextStartLineDetector : TextDeclaration {

        private class Local : ICloneable {
            internal Value Value;

            public object Clone() {
                return new Local() {
                    Value = Value
                };
            }
        }

        static TextStartLineDetector() {
            StartLineFollowedByNewLine = new TextStartLine(true);
            StartLineFollowedByText = new TextStartLine(false);
        }

        private static readonly TextStartLine StartLineFollowedByNewLine;
        private static readonly TextStartLine StartLineFollowedByText;

        private IEnumerable StartLine(Value value) {
            var startLine = StartLineFollowedByText;
            if (value.IsCharacter && Environment.NewLine.Contains((char)value.Character))
                startLine = StartLineFollowedByNewLine;

            return new object[] { startLine, value.Object };
        }
        private IEnumerable StateLine(DeclarationContext frame, Value value) {

            // activate local
            var local = (Local)frame.Local;
            if (local == null) {
                frame.Local = local = new Local();

                // first value is always a start line
                return StartLine(value);
            }

            // subsequent values are start lines if they follow a new line
            var thisCharacter = value.AsCharacter;
            var lastCharacter = local.Value.AsCharacter;

            if (lastCharacter == '\n')
                return StartLine(value);

            if (lastCharacter == '\r' && thisCharacter != '\n')
                return StartLine(value);

            return null;
        }
        private IEnumerable Substitute(DeclarationContext frame, Value value) {
 
            // never inject StartLines when flattening
            if (frame.IsFlattening)
                return null;

            // detect start line
            var substitute = StateLine(frame, value);

            var local = (Local)frame.Local;

            // record if start line encountered...
            local.Value = substitute != null ? 
                default(Value) :    // ...with null value 
                value;              // ...else save the last character

            return substitute;
        }

        protected override IEnumerable Substitute(DeclarationContext frame, char character) {
            return Substitute(frame, new Value(character));
        }
        protected override IEnumerable Substitute(DeclarationContext frame, object value) {
            return Substitute(frame, new Value(value));
        }
    }

    [Declaration(SubstituteTypes = DeclarationAttribute.AllUnknownTypes)]
    public class TextUnknownObjectToString : TextDeclaration {

        protected override IEnumerable Substitute(DeclarationContext frame, object value) {
            if (frame.IsFlattening)
                return null;

            return value.ToString();
        }
    }

    [Declaration(AdaptorType = typeof(Adaptor))]
    public class TextDeclaration : GenericDeclaration, IEnumerable<char> {
        
        protected new class Adaptor : GenericDeclaration.Adaptor {

            public Adaptor(Type adaptedType)
                : base(adaptedType) {
            }

            public override object Prolog(DeclarationContext frame, object value) {

                // when flattening or substantiating 
                // don't detect start lines or convert objects to strings
                if (!(frame.IsEvaluating))
                    return null;

                // don't need to add TextStartLineDetector as root in this case
                if (value is TextStartLineDetector)
                    return null;

                // bail if this is not the first TextDeclaration on the stack
                if (frame.Ancestors(typeof(TextStartLineDetector)).Any())
                    return null;

                // add start line and ToString context
                return new TextStartLineDetector {
                    new TextUnknownObjectToString { value }
                };
            }
            public override Value Substitute(DeclarationContext frame, Value target) {
                var textDeclaration = (TextDeclaration)frame.Object;

                // optimize character case
                if (target.IsCharacter) {
                    var character = (char)target.Character;
                    var result = textDeclaration.Substitute(frame, character);
                    return new Value(result);
                }

                return base.Substitute(frame, target);
            }
        }

        // substitute (optimized to not box characters)
        protected virtual IEnumerable Substitute(DeclarationContext frame, char character) {
            return null;
        }

        // support to convert the declaration to a list of chars, a stream, or a string
        public new IEnumerator<char> GetEnumerator() {
            return Evaluate()
                .Where(o => o.Character.HasValue)
                .Select(o => (char)o.Character)
                .GetEnumerator();
        }
        public TextReader ToTextReader() {
            var characters = Evaluate().Select(o => o.Character);
            return new TextGenerator(characters);
        }
        public TextEncoder ToTextEncoder(Encoding encoding = null, int byteBufferSize = 1024 * 4) {
            if (encoding == null)
                encoding = Encoding.UTF8;
            return new TextEncoder(ToTextReader(), encoding, byteBufferSize);
        }
        public string ReadToEnd() {
            return ToTextReader().ReadToEnd();
        }
    }

    public class TextFlatten : GenericDeclaration, IEnumerable<string> {
        public new IEnumerator<string> GetEnumerator() {
            var flatten = new Flatten { Children() };
            var enumeration = flatten.Select(o => new TextDeclaration { o }.ReadToEnd());
            return enumeration.GetEnumerator();
        }
    }

    [Declaration(UsePhysicalStack = true)]
    public sealed class TextStartLine : GenericDeclaration {

        private bool m_newLineFollows;

        public TextStartLine(bool newLineFollows) {
            m_newLineFollows = newLineFollows;
        }

        public bool NewLineFollows {
            get { return m_newLineFollows; }
        }
    }

    public sealed class TextJoin : TextDeclaration {

        private object m_delimiter;
        private object m_prefix;
        private object m_suffix;
        private object m_lastDelimiter;
        private bool m_force;

        public TextJoin(object delimiter = null,
            object prefix = null,
            object suffix = null,
            bool force = false,
            object lastDelimiter = null) {

            m_lastDelimiter = lastDelimiter;
            m_delimiter = delimiter;
            m_prefix = prefix;
            m_suffix = suffix;
            m_force = force;
        }

        protected override IEnumerable Substance() {
            if (m_force)
                return Substantial;

            return Children();
        }
        protected override IEnumerable Children(DeclarationContext frame) {

            var content = frame.Flatten(Children()).ToArray();

            if (!m_force && !Declaration.IsSubstantial(content))
                yield break;

            var count = 0;
            if (m_lastDelimiter != null)
                count = content.Count();

            yield return m_prefix;

            bool first = true;
            foreach (var o in content) {
                var value = o;

                if (value == null)
                    continue;

                count--;

                if (!Declaration.IsSubstantial(value))
                    continue;

                if (!first) {
                    if (count == 0 && m_lastDelimiter != null)
                        yield return m_lastDelimiter;
                    else
                        yield return m_delimiter;
                }

                first = false;
                yield return value;
            }

            yield return m_suffix;
        }
    }
    public sealed class TextSingleSpaced : TextDeclaration {

        private object m_prefix;
        private object m_suffix;

        public TextSingleSpaced(object prefix = null, object suffix = null) {
            m_prefix = prefix;
            m_suffix = suffix;
        }

        protected override IEnumerable Children(DeclarationContext frame) {
            return new TextJoin(Environment.NewLine,
                prefix: m_prefix, suffix: m_suffix) { Children() };
        }
        protected override IEnumerable Substance() {
            return Children();
        }
    }
    public sealed class TextDoubleSpaced : TextDeclaration {

        private object m_prefix;
        private object m_suffix;

        public TextDoubleSpaced(object prefix = null, object suffix = null) {
            m_prefix = prefix;
            m_suffix = suffix;
        }

        protected override IEnumerable Children(DeclarationContext frame) {
            return new TextJoin(
                Environment.NewLine + Environment.NewLine, prefix: m_prefix, suffix: m_suffix) { Children() };
        }
        protected override IEnumerable Substance() {
            return Children();
        }
    }
    public sealed class TextLine : TextDeclaration {

        protected override IEnumerable Substance() {
            return Children();
        }
        protected override IEnumerable Children(DeclarationContext frame) {
            return new TextJoin(suffix: Environment.NewLine) { Children() };
        }
    }

    public sealed class TextWrap : TextDeclaration {

        private static readonly object Position = new object();

        private int m_lineLength;

        public TextWrap(int lineLength) {
            m_lineLength = lineLength;
        }

        protected override IEnumerable Children(DeclarationContext frame) {
            var text = new TextDeclaration { Children() }.ReadToEnd();
            
            text = Regex.Replace(text, Environment.NewLine, " ");

            int position = 0;

            foreach (var character in text) {

                if (char.IsWhiteSpace(character) && position >= m_lineLength) {
                    yield return Environment.NewLine;
                    position = 0;
                    continue;
                }

                yield return character;
                position++;
            }
        }
    }

    [Declaration(SubstituteTypes = typeof(TextStartLine))]
    public sealed class TextIndentReset : TextDeclaration {

        protected override IEnumerable Substitute(DeclarationContext frame, object value) {
            return Vanish;
        }
    }
    public sealed class TextIndentDefault : TextDeclaration {
        public static readonly object DefaultValue = "    ";

        private object m_value;

        public TextIndentDefault(object value) {
            m_value = value;
        }

        public object Value {
            get { return m_value; }
        }
    }

    [Declaration(SubstituteTypes = typeof(TextStartLine))]
    public sealed class TextIndent : TextDeclaration {

        private object m_indent;

        public TextIndent(int count)
            : this("".PadLeft(count)) {
        }
        public TextIndent(char indent)
            : this((object)indent) {
        }
        public TextIndent(object indent = null) {
            m_indent = indent;
        }

        protected override IEnumerable Substance() {
            return Children();
        }
        protected override IEnumerable Substitute(DeclarationContext frame, object value) {
            var startLine = (TextStartLine)value;

            // do not inject indent next line is a return character
            // (e.g. prevent "\n" => "\t\n")
            if (startLine.NewLineFollows)
                yield break;

            // do not inject indent when flattening 
            // (e.g. prevent TextJoin { "foo", "bar" } => "\t, foo, bar")
            if (frame.IsFlattening)
                yield break;

            var substitute = frame.Substitute(startLine);
            if (substitute != null)
                yield return substitute;

            yield return frame.Local;
        }
        protected override void InitializeFrame(DeclarationContext frame) {
            if (m_indent != null) {
                frame.Local = m_indent;
                return;
            }

            var defaultIndent = frame.Ancestors<TextIndentDefault>().FirstOrDefault();
            if (defaultIndent != null) {
                frame.Local = defaultIndent.Value;
                return;
            }

            frame.Local = TextIndentDefault.DefaultValue;
        }

        public object Value {
            get { return m_indent; }
        }
    }
    public sealed class TextHeader : TextDeclaration {

        private object m_header;
        private object m_footer;

        public TextHeader(object header, object footer = null) {
            m_header = header;
            m_footer = footer;
        }

        protected override IEnumerable Substance() {
            return Children();
        }
        protected override IEnumerable Children(DeclarationContext frame) {
            return new TextSingleSpaced() {
                m_header,
                new TextIndent() {
                    new TextSingleSpaced() {
                        Children()
                    }
                },
                m_footer,
            };
        }
    }

    public sealed class TextNameValue : TextDeclaration {

        public static IEnumerable<KeyValuePair<object, object>> ToKeyValuePairs(object o) {
            if (o == null)
                return null;

            var dictionary = o as IDictionary;
            var keyValues = o as IEnumerable<KeyValuePair<object, object>>;

            if (dictionary != null)
                return dictionary.ToKeyValuePairs();

            else if (keyValues != null)
                return keyValues;

            return o.GetType().Get<FieldInfo>(isInstance: true).ToKeyValuePairs();
        }

        private IEnumerable<KeyValuePair<object, object>> m_nameValue;
        private object m_delimiter;
        private object m_prefix;
        private object m_suffix;
        private object m_join;

        public TextNameValue(object nameValue,
            string delimiter = ", ", string join = " = ",
            object prefix = null, object suffix = null) {

            m_nameValue = ToKeyValuePairs(nameValue);
            m_join = join;
            m_delimiter = delimiter;
            m_prefix = prefix;
            m_suffix = suffix;
        }

        protected override IEnumerable Substance() {
            if (m_nameValue.Any())
                yield return Substantial;
        }
        protected override IEnumerable Children(DeclarationContext frame) {
            return new TextJoin(m_delimiter, m_prefix, m_suffix) {
                m_nameValue.Select(o => new TextDeclaration() { o.Key, m_join, o.Value })
            };
        }
    }

    public sealed class DeclarationMarkup : Declaration {
    }
}
